iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0
IT 管理

Playwright + Test Design + AI Agent:自動化測試實戰系列 第 5

第 05 天:亢龍有悔,泰極否來,否極泰來。

  • 分享至 

  • xImage
  •  

降龍十八掌與打狗棒法,都是丐幫的絕世武學。然而降龍十八掌的第一招「亢龍有悔」,卻蘊含深意。這句話出自《易經.乾卦》上九爻,意指任何事物發展到極致,往往就會迎來轉折甚至失敗。當我們的測試看似沒有問題,更需要反思是否有所遺漏。這正是 Critical Thinking(批判性思維) 的核心:它不只是對外在的質疑,更是對自身的反思。當我們的測試報告看起來完美無缺時,我們更需要停下來,反問自己:「會不會有什麼沒測到的?」唯有保持這種謙卑與警覺,不斷審視自己的盲點,才能避免重蹈「盛極而衰」的覆轍,確保我們的測試品質不會在最關鍵的時刻功虧一簣。

招式演練:撰寫基於資料庫的測試案例

在前幾天我們撰寫的測試案例雖足以驗證基本的使用者流程,但僅止於 UI 層面的驗證仍嫌不足。因為有些問題並不會顯現在畫面上,而是隱藏在資料庫的狀態之中。因此,我們需要進一步設計基於資料庫的測試案例,透過檢查資料庫的值,來驗證整個流程是否真正正確,確保前後端資料的一致性與完整性。

今天我們會做的兩個測試案例要做的演練:

  1. 撰寫 Gherkin 測試案例並且由 AI 協助產生 steps 程式碼,並且確認測試通過。
  2. 透過同一個測試案例,讓 AI 用 AAA 架構撰寫測試案例,透過 Gherkin 語言與 AI 溝通。

案例背景

使用者在前端表單建立一筆訂單後,系統會將該筆訂單資料寫入資料庫。測試案例需要驗證前端建立成功後,資料庫中確實存在相符資料。以 PostgreSQL 為例(MySQL、MSSQL、SQLite 也可類似處理。

Gherkin 測試案例 order.feature

Feature: 建立訂單並驗證資料庫記錄
  為了確保訂單建立功能正確,身為一位已登入的使用者,希望提交訂單後能在資料庫中找到相符的訂單記錄。

  Scenario: 成功建立訂單並寫入資料庫
  Given 我已經登入並位於建立訂單頁
   When 我輸入商品 "1" 支 "iPhone 17"
    And 送出訂單
   Then 系統應該顯示建立成功訊息
    And 資料庫中應該存在一筆商品為 "iPhone 17" 且數量為 1 的訂單記錄

使用 AI 協助撰寫 steps

  1. 建立 order.feature 並且放在 tests/feature 下面
  2. 開啟 GitHub Copilot Chat 並且選擇建立的下面的 implement-steps-with-feature-file.prompt.md
  3. 輸入閱讀 order.feature 並且撰寫對應的 step 步驟,並且通過測試。
  4. AI 會協助建立類似下列的測試程式碼在 tests/steps/order.steps.ts

如果 AI 順利按照 prompt 規則,則會產生類似下列某一版本的測試程式碼,由於沒有提供訂單的頁面或是資料庫的程式碼,AI 會用註解的方式說明,如果我們有提供訂單頁面的資訊,例如程式碼、selector、或是資料庫程式碼,AI 就能夠幫我們產生出 steps 的測試程式碼。

import { expect } from '@playwright/test';
import { createBdd } from 'playwright-bdd';
import { resolve } from 'node:path';

const { Given, When, Then } = createBdd();

Given('我已經登入並位於建立訂單頁', async ({ page }) => {
  // 直接打開本地登入頁
  const loginPagePath = resolve(__dirname, '../fixtures/login.html');
  await page.goto(`file://${loginPagePath}`);
  await page.fill('input[name="username"]', 'testuser');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');
  // 驗證登入成功
  await expect(page.getByText('Welcome')).toBeVisible();
  // 假設登入後自動導向建立訂單頁(或直接在同頁進行)
});

When('我輸入商品 "{int}" 支 "{string}"', async ({ page }, qty, product) => {
  // 假設有商品名稱與數量欄位
  await page.fill('input[name="product"]', product);
  await page.fill('input[name="quantity"]', String(qty));
});

When('送出訂單', async ({ page }) => {
  await page.click('button[type="submit"]');
});

Then('系統應該顯示建立成功訊息', async ({ page }) => {
  await expect(page.getByText('建立成功')).toBeVisible();
});

Then('畫面上應該看到商品 "{string}" 且數量為 {int} 的訂單', async ({ page }, product, qty) => {
  const orderRow = page.locator('.order-list .order-row').first();
  await expect(orderRow.getByText(product)).toBeVisible();
  await expect(orderRow.getByText(String(qty))).toBeVisible();
});

// 真正連資料庫查驗
Then('資料庫中應該存在一筆商品為 "{string}" 且數量為 {int} 的訂單記錄', async ({}, product, qty) => {
  const order = await findOrder(product, qty); // 從 db.ts 匯入的查詢
  expect(order).toBeTruthy();
  expect(order.product).toBe(product);
  expect(order.qty).toBe(qty);
});

程式碼說明

// modules/db.ts
import { Client } from 'pg'; // 假設用 PostgreSQL

export async function findOrder(product: string, qty: number) {
  const client = new Client({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASS,
    database: process.env.DB_NAME,
  });
  await client.connect();

  const res = await client.query(
    'SELECT * FROM orders WHERE product = $1 AND qty = $2 ORDER BY created_at DESC LIMIT 1',
    [product, qty]
  );

  await client.end();
  return res.rows[0];
}

使用 AAA 架構重新撰寫測試案例

可以選擇 playwright-writer.chatmode 並且要求 AI 用 AAA 架構根據 order.feature,允許 AI 要求的步驟,會產生 order-aaa.spec 的測試程式碼,透過這樣的方式,可以讓 AI 撰寫可維護性的測試程式碼。

test.describe('訂單建立功能測試 (AAA 架構)', () => {
  
  test('成功建立訂單並寫入資料庫', async ({ page }) => {
    // 測試意圖:驗證使用者可以成功建立訂單,並且訂單資料正確儲存到資料庫
    
    // ===== ARRANGE (準備階段) =====
    // 設置測試資料
    const testProduct = 'iPhone 17';
    const testQuantity = 1;
    const orderPagePath = resolve(__dirname, 'fixtures', 'order.html');
    
    // 導航到訂單建立頁面並驗證已登入狀態
    await page.goto(`file://${orderPagePath}`);
    await expect(page.locator('#userInfo')).toBeVisible();
    await expect(page.locator('#userInfo h3')).toContainText('已登入使用者: testuser');
    
    // ===== ACT (執行階段) =====
    // 填寫訂單表單
    await page.fill('#productName', testProduct);
    await page.fill('#quantity', testQuantity.toString());
    
    // 提交訂單
    await page.click('#submitOrder');
    
    // 等待處理完成
    await page.waitForTimeout(100);
    
    // ===== ASSERT (驗證階段) =====
    // 1. 驗證使用者介面回饋
    await expect(page.locator('#successMessage')).toBeVisible();
    await expect(page.locator('#successMessage')).toContainText('訂單建立成功!您的訂單已送出並儲存至系統。');
    
    // 2. 驗證訂單摘要顯示
    await expect(page.locator('#orderSummary')).toBeVisible();
    await expect(page.locator('#orderDetails')).toContainText(`商品: ${testProduct}, 數量: ${testQuantity}`);
    
    // 3. 驗證資料庫記錄 - 使用 JavaScript 查詢
    const orderData = await page.evaluate((args) => {
      return window.getOrderFromDatabase(args.productName, args.quantity);
    }, { productName: testProduct, quantity: testQuantity });
    
    expect(orderData).not.toBeNull();
    if (orderData) {
      expect(orderData.product).toBe(testProduct);
      expect(orderData.quantity).toBe(testQuantity);
      expect(orderData.user).toBe('testuser');
      expect(orderData.timestamp).toBeDefined();
    }
    
    // 4. 驗證 DOM 中的資料屬性
    await expect(page.locator('#orderDatabase .order-record')).toHaveAttribute('data-product', testProduct);
    await expect(page.locator('#orderDatabase .order-record')).toHaveAttribute('data-quantity', testQuantity.toString());
    await expect(page.locator('#orderDatabase .order-record')).toHaveClass(/order-created/);
    
    // 5. 驗證表單已清空
    await expect(page.locator('#productName')).toHaveValue('');
    await expect(page.locator('#quantity')).toHaveValue('');
  });

秘笈傳授:

implement-steps-with-feature-file.prompt.md 撰寫基於 feature 檔案實作 step 步驟的 prompt

mode: agent
tools: ['editFiles', 'search', 'runCommands', 'runTasks', 'problems', 'changes', 'testFailure', 'fetch', 'runTests', 'playwright']
model: 'Claude Sonnet 4'
---
## 角色與目的

**你的目標:** 根據提供的 Feature File,自動產生一系列的測試步驟(Steps)。
**你的原則:** 必須在完成所有指定步驟後,才能生成最終結果。

---

## 具體指令

* 你會收到一個 Feature File。如果使用者沒有提供,請主動請他們提供。
* **切勿**在未完成所有指定步驟的情況下,過早生成任何步驟。
* 請使用 **Playwright MCP** 提供的工具,逐一執行所有步驟。
* **只有在**所有步驟都成功完成後,才根據訊息歷史記錄,生成完整的測試步驟程式碼。
* 將生成的步驟儲存為 Markdown 格式的檔案。
* **檔案路徑:**
    * Feature File:`tests/feature` 目錄。
    * Steps File:`tests/steps` 目錄。
* **檔案命名:** 輸出檔名必須與輸入檔名一致,例如:`sample.feature` → `sample.steps.md`。

---

## 測試執行方式

`npx bddgen && npx playwright test`

playwright-writer.chatmode.md 是我這是使用 chatmode 的角色,可以定義回應的角色的目的和回應風格,並且工具使用的原則,還有最佳準則。

description: 'Playwright Writer — 以「可執行、可維護、可擴充」為目標,協助產生、重構與審查 Playwright 測試。'
tools: ['editFiles', 'runNotebooks', 'search', 'new', 'runCommands', 'runTasks', 'usages', 'vscodeAPI', 'problems', 'changes', 'testFailure', 'openSimpleBrowser', 'fetch', 'githubRepo', 'extensions', 'runTests', 'playwright']
model: 'Claude Sonnet 4'
---
## 角色與目的

你是「Playwright Writer」。你的任務是將自然語言需求轉換為高品質的 Playwright 測試與相關產物(測試資料、Page Object Model、fixtures、CI 設定),並在必要時解釋設計考量與最佳實踐。

---

## 回應風格
* **Code-First**:先給出**最小可執行的程式碼範例**,接著再進行解釋。
* **繁體中文**:所有回應皆使用**繁體中文(zh-TW)**。
* **檔案導向**:產出的內容應包含**檔案樹與檔案內容**,且每段程式碼皆標註檔名。
* **優先語言**:優先提供 **TypeScript** 範例,除非使用者明確指定 Python、JavaScript 或 .NET。
* **測試品質**:所有測試皆需包含:**測試意圖註解**、**選擇器策略說明**、**可重試與等待策略**,以及**資料前置/清理**。
* **多重選項**:當有疑慮時,提供**至少兩種解決方案**並分析其利弊。

---

## 工具使用原則
* **playwright**:用於**建立/執行測試**、產出 **trace** 與**代碼片段**。
* **bash / node**:用於**初始化專案**、**安裝依賴**或**產出腳本**。
* **git**:提供建議的 **commit 訊息**與 **Pull Request 描述**(含測試範圍)。
* **browser**:說明如何透過 **UI 偵測器或錄製器**取得選擇器(僅提供文字說明,不自動瀏覽)。
* **repo**:讀取現有專案目錄,避免重複定義。

---

## 核心準則
1.  **正確性**:**優先使用穩定的定位元素**(如 `role`、`label`、`test-id`),避免使用脆弱的 CSS 或 XPath。
2.  **可維護性**:推廣 **Page Object Model**、**fixtures** 與**有擴展性的命名**。
3.  **可觀測性**:在錯誤處附加 `test.info().attach`,並保留 **trace、screenshot、video** 等產物。
4.  **速度與穩定**:利用**並行測試**、**避免多餘等待**,並善用 `expect.poll` 與內建的 `auto-wait` 機制。
5.  **覆蓋度**:測試應以**使用者路徑、邊界條件與錯誤流程**為導向,至少覆蓋 Happy Path、Unhappy Path 和 Edge Case。

最佳實踐

必須先理解 AAA 和 Given-When-Then 這些基本架構,才能寫出高品質的測試程式碼。同時,還要以『可執行、可維護、可擴充』為目標,不斷調整 prompt 和 chatmode,才能確保 AI 的產生的程式碼與我們撰寫的一致,最後 AI 才能取代自己撰寫程式碼,但測試案例建議還是自己要思考。

收功:今日總結

本日我們通過撰寫 prompt 和 chatmode 讓 AI 幫助我們撰寫測試程式碼,如果沒有給予原則與規範,撰寫出來的程式碼會與我們的風格不符合,可能會導致後續的維護困難,就像是我們用全力撰寫程式碼,卻忘了停下腳步看看程式碼是否易於閱讀和維護,別忘了「亢龍有悔,泰極否來,否極泰來」


上一篇
第 04 天:陰陽平衡,方能登峰造極
系列文
Playwright + Test Design + AI Agent:自動化測試實戰5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言